Recently, Jeffrey Rosenbluth published (and showcased on Reddit) a pretty cool Haskell package called static-canvas. This package uses the free monad DSL pattern to make a DSL for programming for HTML5 canvas
, restricted to fairly simple static use cases. While you can't use this to make user interfaces, it's still potentially a pretty cool tool, and there's a few very clear examples on the GitHub readme.
As with most things involving pretty graphics or pictures, I think this would be a whole ton of fun to experiment with interactively, making it a great fit for IHaskell, an interactive notebook-based environment for Haskell.
IHaskell allows the creation of "addon" packages to specify how to display various data types in its browser-based UI. These addons can render data types as text, as images, or even as HTML mixed with Javascript; they can even render them as interactive Javascript widgets that can evaluate Haskell code at will. All of this is done without GHCJS or similar Haskell-to-Javascript compilation tools.
However, these display packages have mostly been written by only a few people, those fairly closely involved with IHaskell development. As the creator of IHaskell, I'd love to have more of these packages, but I obviously can't create display instances for all existing packages, and certainly can't anticipate what people might want for their own packages or new ones. Thus, I'd love to use this very neat library as a showcase and tutorial for how to make IHaskell display packages.
In this section, I'll very briefly introduce you to the tools IHaskell provides for creating IHaskell display packages. If you'd like to get to the real meat of this tutorial, skip this, read the next section, and maybe come back here if you need to.
IHaskell internally uses a data type called Display
to represent possible outputs. The Display
data types looks like this:
-- In IHaskell.Display
data Display = Display [DisplayData] -- Display just one thing.
| ManyDisplay [Display] -- Display several things.
In turn, the DisplayData
data type from the ipython-kernel
package specifies how to actually display the object in the browser:
-- In IHaskell.IPython.Display
data DisplayData = DisplayData MimeType Text
-- All the possible ways to display things.
data MimeType = PlainText
| MimeHtml
| MimePng Width Height -- Base64 encoded.
| MimeJpg Width Height -- Base64 encoded.
| MimeSvg
| MimeLatex
| MimeJavascript
For example, to output the string "Hello" in red in the browser, you might construct a value like this:
redStr :: Display
redStr = Display [textDisplay, htmlDisplay]
textDisplay :: DisplayData
textDisplay = DisplayData PlainText "Hello"
htmlDisplay :: DisplayData
htmlDisplay = DisplayData MimeHtml "<span style=\"color: red;\">Hello</span>"
You may note that Display
takes a list of DisplayData
values; this allows IHaskell to choose the proper display mechanism for the frontend. The frontend can be a console or the in-browser notebook, and the in-browser notebook may have different preferences for displays, so by providing different ways to render output, the best possible rendering can be chosen for each interface.
Instead of always using the data types, IHaskell.Display
exports the following convenience functions:
-- Construct displays from raw strings of different types.
plain :: String -> DisplayData
html :: String -> DisplayData
svg :: String -> DisplayData
latex :: String -> DisplayData
javascript :: String -> DisplayData
-- Encode into base 64.
encode64 :: String -> Base64
decode64 :: ByteString -> Base64
-- Display images.
png :: Int -> Int -> Base64 -> DisplayData
jpg :: Int -> Int -> Base64 -> DisplayData
-- Create final Displays.
Display :: [DisplayData] -> Display
many :: [Display] -> Display
In order to create a display for some data type, we must first import the main IHaskell display module:
import IHaskell.Display
This package contains the following typeclass:
class IHaskellDisplay a where
display :: a -> IO Display
In order to display a data type, create an instance of IHaskellDisplay
for your data type – then, any expression that results in your data type will generate a corresponding display.
Let's go ahead and do this for CanvasFree a
from the static-canvas
package.
In [12]:
-- Start with necessary imports.
import IHaskell.Display -- From the 'ihaskell' package.
import IHaskell.IPython.Types(MimeType(..))
import Graphics.Static -- From the 'static-canvas' package.
-- Text conversion functions.
import Data.Text.Lazy.Builder(toLazyText)
import Data.Text.Lazy(toStrict)
Now that we have the imports out of the way, we can define the core instance necessary:
In [24]:
-- Since CanvasFree is a type synonym, we need a language pragma.
{-# LANGUAGE TypeSynonymInstances #-}
instance IHaskellDisplay (CanvasFree ()) where
-- display :: CanvasFree () -> IO Display
display canvas = return $
let src = toStrict $ toLazyText $ buildScript width height canvas
in Display [DisplayData MimeHtml src]
where (height, width) = (200, 600)
We can now copy and paste the examples from the static-canvas
Github page, and see them appear right in the notebook!
In [34]:
{-# LANGUAGE OverloadedStrings #-}
import Graphics.Static.ColorNames
text :: CanvasFree ()
text = do
font "italic 60pt Calibri"
lineWidth 6
strokeStyle blue
fillStyle goldenrod
textBaseline TextBaselineMiddle
strokeText "Hello" 150 100
fillText "Hello World!" 150 100
text
As we play with this a little more, we see that this is a little bit unsatisfactory. Specifically, the width and the height of the resulting canvas are fixed in the IHaskellDisplay
instance! I would solve this by creating a custom Canvas
data type that stores these:
In [26]:
data Canvas = Canvas {
width :: Int,
height :: Int,
canvas :: CanvasFree ()
}
Then we could define an IHaskellDisplay
that respects this width and height:
In [27]:
{-# LANGUAGE TypeSynonymInstances #-}
instance IHaskellDisplay Canvas where
-- display :: Canvas -> IO Display
display cnv = return $
let src = toStrict $ toLazyText $ buildScript (width cnv) (height cnv) (canvas cnv)
in Display [DisplayData MimeHtml src]
Then when we use this we can specify how to display our canvases:
Canvas 200 600 $ do
font "italic 60pt Calibri"
lineWidth 6
strokeStyle blue
fillStyle goldenrod
textBaseline TextBaselineMiddle
strokeText "Hello" 150 100
fillText "Hello World!" 150 100
Sadly, it seems that the static-canvas
library currently only supports having one generated canvas on the page – if you try to add another one, it simply modifies the pre-existing one. This is probably a bug that should be fixed, though!
Once you've made an IHaskell display instance, you can easily package it up and stick it on Hackage. Specifically, for a package named package-name
, you should take everything before the -
. Then, prepend ihaskell-
to the package name. Finally, make sure there exists a module IHaskell.Display.Package
, where Package
is the first word in package-name
capitalized. If this is done, then IHaskell will happily load your package and instance upon startup, making it very easy for your users to install the display addon!
For example, the hatex
library is exposed as an addon through the ihaskell-hatex
display package and the IHaskell.Display.Hatex
module in that package. The juicypixels
library has an addon package called ihaskell-juicypixels
with a module IHaskell.Display.Juicypixels
.
As I write this now, I realize that this protocol is a little bit weird. Specifically, I think that perhaps the rule that you take the first thing before the -
is not too great, but rather that perhaps the -
should be a word separator, and thus package-name
would get translated to ihaskell-package-name
and IHaskell.Display.PackageName
. (We do need some standard!)
If you have any opinions about this, or suggestions for how to improve this process, please let me know!
Anyway, I hope that this brief tutorial / guide can show someone how to write small IHaskell addons. Perhaps someone will find this useful, and please get in touch if you have any questions, comments, or suggestions!